﻿using System;
using System.Net;
using System.Security;

namespace SocialKit.LightRest
{
    /// <summary>
    /// Represents the http client that consumes REST services.
    /// </summary>
    public class RestClient
    {
        const string userAgent = "SocialKit.LightRest v0.2.1";

        Uri baseAddress;

        /// <summary>
        /// Gets or sets the base Uri of current RestClient.
        /// </summary>
        public Uri BaseAddress
        {
            get { return this.baseAddress; }
            set
            {
                if (value != null)
                {
                    if (!value.IsAbsoluteUri)
                    {
                        throw new UriFormatException(value + " is not an absolute URI.");
                    }
                }

                this.baseAddress = value;
            }
        }

        HttpWebRequestTransportSettings settings;

        /// <summary>
        /// Gets or sets the HttpWebRequestTransportSettings used for sending request messages.
        /// </summary>
        public HttpWebRequestTransportSettings TransportSettings
        {
            get
            {
                if (this.settings == null)
                    this.settings = new HttpWebRequestTransportSettings();

                return this.settings;
            }

            set { this.settings = value; }
        }

        /// <summary>
        /// Create an instance of SocialKit.LightRest.RestClient class.
        /// </summary>
        public RestClient()
        {
        }

        /// <summary>
        /// Create an instance of SocialKit.LightRest.RestClient class with specified base Uri string.
        /// </summary>
        /// <param name="baseAddress">The base uri address string.</param>
        public RestClient(string baseAddress) :
            this(new Uri(baseAddress, UriKind.Absolute))
        {
        }

        /// <summary>
        /// Create an instance of SocialKit.LightRest.RestClient class with specified base Uri.
        /// </summary>
        /// <param name="baseAddress">The base Uri.</param>
        public RestClient(Uri baseAddress)
        {
            this.BaseAddress = baseAddress;
        }

        #region Sync Send

        /// <summary>
        /// Send the specified HttpRequestMessage.
        /// </summary>
        /// <param name="request">The request message to be sent.</param>
        /// <returns></returns>
        public HttpResponseMessage Send(HttpRequestMessage request)
        {
            PrepareRequest(request);

            var http = (HttpWebRequest)WebRequest.Create(request.Uri);

            http.Method = request.Method;
            http.UserAgent = userAgent;

            if (!string.IsNullOrEmpty(request.ContentType))
            {
                http.ContentType = request.ContentType;
            }

            if (request.ContentLength.HasValue)
            {
                http.ContentLength = request.ContentLength.Value;

                using (var stream = http.GetRequestStream())
                {
                    if (request.ContentLength > 0)
                        stream.Write(request.Content, 0, request.Content.Length);
                }
            }

            CopySettingsToHttpWebRequest(this.TransportSettings, http);

            try
            {
                var httpResponse = (HttpWebResponse)http.GetResponse();

                return new HttpResponseMessage(httpResponse);
            }
            catch (WebException wex)
            {
                if (wex.Response != null)
                {
                    return new HttpResponseMessage((HttpWebResponse)wex.Response);
                }

                throw wex;
            }
        }

        /// <summary>
        /// Send an HttpRequestMessage with specified method to a Uri and contain 
        /// the request http body created by the creator. 
        /// </summary>
        /// <param name="method">The method used to send request.</param>
        /// <param name="uri">A Uri to request.</param>
        /// <param name="creator">A request http body creator.</param>
        /// <returns></returns>
        public HttpResponseMessage Send(HttpMethod method, Uri uri, IHttpContentCreator creator)
        {
            var request = new HttpRequestMessage(method.ToString(), uri, creator);

            return Send(request);
        }

        /// <summary>
        /// Send an HttpRequestMessage with specified method to a Uri.
        /// </summary>
        /// <param name="method">The method used to send request.</param>
        /// <param name="uri">A Uri to request.</param>
        /// <returns></returns>
        public HttpResponseMessage Send(HttpMethod method, Uri uri)
        {
            var request = new HttpRequestMessage(method.ToString(), uri);

            return Send(request);
        }

        /// <summary>
        /// Send an HttpRequestMessage with specified method to a Uri string and contain 
        /// the request http body created by the creator. 
        /// </summary>
        /// <param name="method">The method used to send request.</param>
        /// <param name="uri">A Uri string to request.</param>
        /// <param name="creator">A request http body creator.</param>
        /// <returns></returns>
        public HttpResponseMessage Send(HttpMethod method, string uri, IHttpContentCreator creator)
        {
            return Send(method, new Uri(uri, UriKind.RelativeOrAbsolute), creator);
        }

        /// <summary>
        /// Send an HttpRequestMessage with specified method to a Uri string.
        /// </summary>
        /// <param name="method">The method used to send request.</param>
        /// <param name="uri">A Uri string to request.</param>
        /// <returns></returns>
        public HttpResponseMessage Send(HttpMethod method, string uri)
        {
            return Send(method, new Uri(uri, UriKind.RelativeOrAbsolute));
        }

        /// <summary>
        /// Send an HttpRequestMessage with specified method to the BaseAddress of current RestClient.
        /// </summary>
        /// <param name="method">The method used to send request.</param>
        /// <returns></returns>
        public HttpResponseMessage Send(HttpMethod method)
        {
            return Send(method, this.BaseAddress);
        }

        #endregion

        #region Async Send

        /// <summary>
        /// Occurs when received the http response after an HttpRequestMessage has been sent asynchronously.
        /// </summary>
        public event EventHandler<SendAsyncCompletedEventArgs> SendAsyncCompleted;

        /// <summary>
        /// Send an HttpRequestMessage asynchronously.
        /// </summary>
        /// <param name="request">The request message to be sent.</param>
        /// <param name="userState">User specified unique identifier for the asynchronous task.</param>
        public void SendAsync(HttpRequestMessage request, object userState)
        {
            PrepareRequest(request);

            var http = (HttpWebRequest)WebRequest.Create(request.Uri);

            http.Method = request.Method;
            http.UserAgent = userAgent;

            CopySettingsToHttpWebRequest(this.TransportSettings, http);

            if (!string.IsNullOrEmpty(request.ContentType))
            {
                http.ContentType = request.ContentType;
            }

            if (request.ContentLength.HasValue)
            {
                http.ContentLength = request.ContentLength.Value;

                http.BeginGetRequestStream(ar =>
                {
                    using (var stream = http.EndGetRequestStream(ar))
                    {
                        if (request.ContentLength > 0)
                            stream.Write(request.Content, 0, request.Content.Length);
                    }

                    http.BeginGetResponse(ProcessResponse, new SendAsyncState(this, http, userState));

                }, null);
            }
            else
            {
                http.BeginGetResponse(ProcessResponse, new SendAsyncState(this, http, userState));
            }
        }

        /// <summary>
        /// Send an HttpRequestMessage asynchronously with specified method to a Uri and
        /// contain the request http body created by the creator. 
        /// </summary>
        /// <param name="method">The method used to send request.</param>
        /// <param name="uri">A Uri to request.</param>
        /// <param name="creator">A request http body creator.</param>
        /// <param name="userState">User specified unique identifier for the asynchronous task.</param>
        public void SendAsync(HttpMethod method, Uri uri, IHttpContentCreator creator, object userState)
        {
            var request = new HttpRequestMessage(method.ToString(), uri, creator);

            SendAsync(request, userState);
        }

        /// <summary>
        /// Send an HttpRequestMessage asynchronously with specified method to a Uri.
        /// </summary>
        /// <param name="method">The method used to send request.</param>
        /// <param name="uri">A Uri to request.</param>
        /// <param name="userState">User specified unique identifier for the asynchronous task.</param>
        /// <returns></returns>
        public void SendAsync(HttpMethod method, Uri uri, object userState)
        {
            var request = new HttpRequestMessage(method.ToString(), uri);

            SendAsync(request, userState);
        }

        /// <summary>
        /// Send an HttpRequestMessage asynchronously with specified method to a Uri string and
        /// contain the request http body created by the creator. 
        /// </summary>
        /// <param name="method">The method used to send request.</param>
        /// <param name="uri">A Uri string to request.</param>
        /// <param name="creator">A request http body creator.</param>
        /// <param name="userState">User specified unique identifier for the asynchronous task.</param>
        public void SendAsync(HttpMethod method, string uri, IHttpContentCreator creator, object userState)
        {
            SendAsync(method, new Uri(uri, UriKind.RelativeOrAbsolute), creator, userState);
        }

        /// <summary>
        /// Send an HttpRequestMessage asynchronously with specified method to a Uri string.
        /// </summary>
        /// <param name="method">The method used to send request.</param>
        /// <param name="uri">A Uri string to request.</param>
        /// <param name="userState">User specified unique identifier for the asynchronous task.</param>
        public void SendAsync(HttpMethod method, string uri, object userState)
        {
            SendAsync(method, new Uri(uri, UriKind.RelativeOrAbsolute), userState);
        }

        /// <summary>
        /// Send an HttpRequestMessage asynchronously with specified method to the BaseAddress of current RestClient.
        /// </summary>
        /// <param name="method">The method used to send request.</param>
        /// <param name="userState">User specified unique identifier for the asynchronous task.</param>
        public void SendAsync(HttpMethod method, object userState)
        {
            SendAsync(method, this.BaseAddress, userState);
        }

        #endregion

        #region Helpers

        void PrepareRequest(HttpRequestMessage request)
        {
            if (request == null)
            {
                if (this.baseAddress == null)
                {
                    throw new ArgumentNullException("request", "The request is null and BaseAddress is null too.");
                }

                request = new HttpRequestMessage();
            }

            if (request.Uri == null)
            {
                if (this.baseAddress == null)
                {
                    throw new ArgumentNullException("request", "request.Uri is null and BaseAddress is null too.");
                }

                request.Uri = this.BaseAddress;
            }
            else if (!request.Uri.IsAbsoluteUri)
            {
                if (this.baseAddress == null)
                {
                    throw new UriFormatException("request.Uri is relative (" + request.Uri + ") and BaseAddress is null.");
                }

                request.Uri = new Uri(this.BaseAddress, request.Uri);
            }

            if (string.IsNullOrEmpty(request.Method))
            {
                request.Method = HttpMethod.GET.ToString();
            }
        }

        static void ProcessResponse(IAsyncResult ar)
        {
            var state = (SendAsyncState)ar.AsyncState;

            var client = state.Client;
            var http = state.Request;
            var userState = state.UserState;

            HttpResponseMessage response = null;
            Exception error = null;

            try
            {
                var httpResponse = (HttpWebResponse)http.EndGetResponse(ar);

                response = new HttpResponseMessage(httpResponse);
            }
            catch (WebException wex)
            {
                error = wex;

                if (wex.Response != null)
                {
                    response = new HttpResponseMessage((HttpWebResponse)wex.Response);
                }
            }

            if (client.SendAsyncCompleted != null)
            {
                client.SendAsyncCompleted(client, new SendAsyncCompletedEventArgs(response, error, userState));
            }
        }

        static void CopySettingsToHttpWebRequest(HttpWebRequestTransportSettings settings, HttpWebRequest http)
        {
            // From WCF Rest starter kit :)

            // skip: request.ConnectionGroupName;
            // skip: request.ContinueDelegate;
            // skip: request.KeepAlive;
            // skip: request.Pipelined;
            // skip: request.ProtocolVersion;
            // skip: request.ServicePoint
            // skip: request.UnsafeAuthenticatedConnectionSharing;

#if !PocketPC

            if (settings.AuthenticationLevel != null)
            {
                http.AuthenticationLevel = settings.AuthenticationLevel.Value;
            }

            if (settings.HasCachePolicy)
            {
                http.CachePolicy = settings.CachePolicy;
            }

            if (settings.ImpersonationLevel != null)
            {
                http.ImpersonationLevel = settings.ImpersonationLevel.Value;
            }

            if (settings.MaximumResponseHeaderKB != null)
            {
                http.MaximumResponseHeadersLength = settings.MaximumResponseHeaderKB.Value;
            }

            if (settings.Cookies != null)
            {
                http.CookieContainer = settings.Cookies;
            }

            if (settings.UseDefaultCredentials != null)
            {
                http.UseDefaultCredentials = settings.UseDefaultCredentials.Value;
            }

#endif

            if (settings.AutomaticDecompression != null)
            {
                http.AutomaticDecompression = settings.AutomaticDecompression.Value;
            }

            if (settings.AllowWriteStreamBuffering != null)
            {
                http.AllowWriteStreamBuffering = settings.AllowWriteStreamBuffering.Value;
            }

            if (settings.HasClientCertificates)
            {
                http.ClientCertificates = settings.ClientCertificates;
            }

            if (settings.ReadWriteTimeout != null)
            {
                http.ReadWriteTimeout = (int)settings.ReadWriteTimeout.Value.TotalMilliseconds;
            }

            if (settings.MaximumAutomaticRedirections != 0)
            {
                http.AllowAutoRedirect = true;
                http.MaximumAutomaticRedirections = settings.MaximumAutomaticRedirections;
            }
            else
            {
                http.AllowAutoRedirect = false;
            }

            if (settings.PreAuthenticate != null)
            {
                http.PreAuthenticate = settings.PreAuthenticate.Value;
            }

            if (settings.Credentials != null)
            {
                http.Credentials = settings.Credentials;
            }

            if (settings.HasProxy)
            {
                http.Proxy = settings.Proxy;
            }

            if (settings.SendChunked != null)
            {
                http.SendChunked = settings.SendChunked.Value;
            }

            if (settings.ConnectionTimeout != null)
            {
                http.Timeout = (int)settings.ConnectionTimeout.Value.TotalMilliseconds;
            }
        }

        #endregion
    }

    class SendAsyncState
    {
        public RestClient Client { get; private set; }
        public HttpWebRequest Request { get; private set; }
        public object UserState { get; private set; }

        public SendAsyncState(RestClient client, HttpWebRequest request, object userState)
        {
            this.Client = client;
            this.Request = request;
            this.UserState = userState;
        }
    }
}